/**
 *       Copyright (c) 2012 Lukas Zaruba
 *
 *   This file is part of Robot Playground.
 *
 *   Robot Playground is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU Lesser General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   Robot Playground is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU Lesser General Public License for more details.
 *
 *   You should have received a copy of the GNU Lesser General Public License
 *   along with Robot Playground. If not, see <http://www.gnu.org/licenses/>.
 */
package net.zarubsys.robotplayground.gui.session.editor;

import java.io.File;
import java.io.IOException;
import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import net.zarubsys.commons.utils.StringUtils;
import net.zarubsys.robotplayground.core.session.DeviceSessionContainer;
import net.zarubsys.robotplayground.core.session.ModuleSessionContainer;
import net.zarubsys.robotplayground.core.session.ProgramSessionContainer;
import net.zarubsys.robotplayground.core.session.Session;
import net.zarubsys.robotplayground.core.session.SessionsHelper;
import net.zarubsys.robotplayground.gui.plugin.GUIPlugin;
import net.zarubsys.robotplayground.gui.session.plugin.SessionGUIPlugin;
import net.zarubsys.robotplayground.gui.session.providers.IOpenable;
import net.zarubsys.robotplayground.gui.session.providers.ISystemObjectChangeListener;
import net.zarubsys.robotplayground.gui.session.providers.ISystemObjectChangeNotifier;
import net.zarubsys.robotplayground.gui.session.providers.ISystemObjectGUIProvider;
import net.zarubsys.robotplayground.gui.session.providers.SystemObjectGUIProviderClass;
import net.zarubsys.robotplayground.gui.session.providers.SystemObjectGUIProvidersHelper;

import org.apache.log4j.Logger;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.ResourcesPlugin;
import org.eclipse.core.runtime.Assert;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.IPath;
import org.eclipse.core.runtime.IProgressMonitor;
import org.eclipse.core.runtime.NullProgressMonitor;
import org.eclipse.core.runtime.Platform;
import org.eclipse.jface.dialogs.MessageDialog;
import org.eclipse.jface.viewers.BaseLabelProvider;
import org.eclipse.jface.viewers.ILabelProvider;
import org.eclipse.jface.window.Window;
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ModifyEvent;
import org.eclipse.swt.events.ModifyListener;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.graphics.Image;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.layout.RowLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.ExpandBar;
import org.eclipse.swt.widgets.ExpandItem;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Text;
import org.eclipse.ui.IEditorInput;
import org.eclipse.ui.IEditorSite;
import org.eclipse.ui.IPathEditorInput;
import org.eclipse.ui.PartInitException;
import org.eclipse.ui.PlatformUI;
import org.eclipse.ui.dialogs.ElementListSelectionDialog;
import org.eclipse.ui.dialogs.SaveAsDialog;
import org.eclipse.ui.part.EditorPart;

/**
 * SessionEditor
 *
 * @author Lukas Zaruba, lukas.zaruba@gmail.com
 */
public class SessionEditor extends EditorPart {
	
	private static final Logger LOG = Logger.getLogger(SessionEditor.class);
	
	private boolean dirty = false;
	private Session session;
	private IPath sessionPath;

	private Label errorMessage;
	private Text name;
	private Text description;
	
	private List<SystemObjectValidator> validators;
	
	public Session getSession() {
		return session;
	}
	
	public IPath getSessionPath() {
		return sessionPath;
	}

	@Override
	public void doSave(IProgressMonitor monitor) {
		IPathEditorInput pathInput = (IPathEditorInput) getEditorInput();
		File file = pathInput.getPath().toFile();
		saveInternal(monitor, file);
		monitor.done();
	}

	@Override
	public void doSaveAs() {
		SaveAsDialog dialog = new SaveAsDialog(Display.getCurrent().getActiveShell());
		dialog.setOriginalName(getEditorInput().getName());
		if (dialog.open() != Window.OK) {
			return;
		}
		
		IPath ws = Platform.getLocation();
		IPath resPath = dialog.getResult();
		IPath path = ws.append(resPath);
		
		File file = path.toFile();
		if (!file.exists()) {
			boolean created = false;
			try {
				created = file.createNewFile(); 
			} catch (IOException e) {
				MessageDialog errorDialog = new MessageDialog(Display.getCurrent().getActiveShell(), "Cannot create file", 
						null, "File \"" + file.getAbsolutePath() + "\" cannot be created.\nError message: " + e.getMessage(), 
						MessageDialog.ERROR, new String[] {"OK"}, 0);
				errorDialog.open();
				return;
			}
			
			if (!created) {
				MessageDialog errorDialog = new MessageDialog(Display.getCurrent().getActiveShell(), "Cannot create file", 
						null, "File \"" + file.getAbsolutePath() + "\" cannot be created.", 
						MessageDialog.ERROR, new String[] {"OK"}, 0);
				errorDialog.open();
			}
		}
		
		saveInternal(new NullProgressMonitor(), file);
		
		IFile resFile = ResourcesPlugin.getWorkspace().getRoot().getFile(resPath);
		try {
			resFile.refreshLocal(1, new NullProgressMonitor());
		} catch (CoreException e) {
			LOG.error("Error refreshing Workspace after save as...", e);
			// ignored
		}
	}
	
	private void saveInternal(IProgressMonitor monitor, File file) {
		boolean valid = validate();
		monitor.worked(1);
		if (!valid) {
			MessageDialog dialog = new MessageDialog(Display.getCurrent().getActiveShell(), "Session contains error(s)!", 
					null, "Do you want to save it anyway?\nNote: This session cannot be run until it is fixed.", 
					MessageDialog.WARNING, new String[] {"Yes", "No"}, 1);
			boolean saveAnyway = dialog.open() == 0;
			if (!saveAnyway) return;
		}
		
		monitor.beginTask("Saving...", 4);
		monitor.worked(1);
		
		SessionsHelper.getInstance().saveSession(session, file);
		
		dirty = false;
		firePropertyChange(PROP_DIRTY);
	}

	@Override
	public void init(IEditorSite site, IEditorInput input) throws PartInitException {
		setSite(site);
		if (!(input instanceof IPathEditorInput)) {
			throw new PartInitException("Invalid input: Must be IPathEditorInput");
		} 
		setInput(input);
		
		IPathEditorInput pathInput = (IPathEditorInput) input;
		sessionPath = loadSessionFile(pathInput);
		session = getSessionSettings(sessionPath.toFile());
		setPartName(pathInput.getName());
	}
	
	private IPath loadSessionFile(IPathEditorInput pathInput) {
		return pathInput.getPath();
	}
	
	private Session getSessionSettings(File file) {
		return SessionsHelper.getInstance().loadSession(file);
	}

	@Override
	public boolean isDirty() {
		return dirty;
	}

	@Override
	public boolean isSaveAsAllowed() {
		return true;
	}

	@Override
	public void createPartControl(Composite parent) {
		Label label;
		GridData gd;
		
		Composite composite = new Composite(parent, SWT.NONE);
		gd = new GridData(SWT.FILL, SWT.FILL, true, true);
		composite.setLayoutData(gd);
		composite.setLayout(new GridLayout(1, false));
		
		errorMessage = new Label(composite, SWT.NONE);
		gd = new GridData(SWT.FILL, SWT.CENTER, true, false);
		gd.heightHint = 15;
		errorMessage.setLayoutData(gd);
		errorMessage.setForeground(Display.getDefault().getSystemColor(SWT.COLOR_RED));
		
		label = new Label(composite, SWT.NONE);
		label.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, false, false));
		label.setText("Name:");
		
		name = new Text(composite, SWT.SINGLE | SWT.LEAD | SWT.BORDER);
		gd = new GridData(SWT.FILL, SWT.CENTER, true, false);
		gd.widthHint = 300;
		name.setLayoutData(gd);
		name.setText(session.getName() == null ? "" : session.getName());
		name.addModifyListener(new ModifyListener() {

			public void modifyText(ModifyEvent e) {
				Text t = (Text) e.widget;
				session.setName(t.getText());
				validate();
				dirty = true;
				firePropertyChange(PROP_DIRTY);
			}
			
		});
		
		label = new Label(composite, SWT.NONE);
		label.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, false, false))	;
		label.setText("Description:");
		
		description = new Text(composite, SWT.MULTI | SWT.LEAD | SWT.BORDER);
		gd = new GridData(SWT.FILL, SWT.CENTER, false, false);
		gd.heightHint = 50;
		description.setLayoutData(gd);
		description.setText(session.getDescription() == null ? "" : session.getDescription());
		description.addModifyListener(new ModifyListener() {
			
			public void modifyText(ModifyEvent e) {
				session.setDescription(description.getText());
				validate();
				dirty = true;
				firePropertyChange(PROP_DIRTY);
			}
			
		});
		
		label = new Label(composite, SWT.NONE);
		label.setLayoutData(new GridData(SWT.BEGINNING, SWT.CENTER, false, false))	;
		label.setText("Last Run: " + (session.getLastRun() == null ? "never" : DateFormat.getDateInstance(DateFormat.MEDIUM).format(session.getLastRun())));
		
		Label separator = new Label(composite, SWT.SEPARATOR | SWT.HORIZONTAL);
		gd = new GridData();
		gd.horizontalAlignment = GridData.FILL;
		gd.grabExcessHorizontalSpace = true;
		gd.heightHint = 20;
		separator.setLayoutData(gd);
		
		ExpandBar expandBar = new ExpandBar(composite, SWT.V_SCROLL);
		gd = new GridData(SWT.FILL, SWT.FILL, true, true);
		expandBar.setLayoutData(gd);
		
		validators = new ArrayList<>();
		
		createProgramsPart(expandBar);
		createMonitorsPart(expandBar);
		createModulesPart(expandBar);
		createDevicesPart(expandBar);
		
		validate();
	}
	
	private void createExpandingPart(final ExpandBar parent, final SystemObjectGUIProviderClass klass, List<String> objectIds, String label, boolean expanded) {
		final Composite expandItemComposite = new Composite(parent, SWT.NONE);
		GridLayout gridLayout = new GridLayout(1, false);
		gridLayout.verticalSpacing = 0;
		expandItemComposite.setLayout(gridLayout);
		
		
		Label plusLabel = new Label(expandItemComposite, SWT.CENTER);
		GridData gd = new GridData(SWT.RIGHT, SWT.CENTER, true, false);
		plusLabel.setLayoutData(gd);
		plusLabel.setImage(GUIPlugin.getDefault().getImage(GUIPlugin.PLUS_IMG));
		
		final ExpandItem expandItem = new ExpandItem(parent, SWT.NONE);
		final Composite objectsComposite = new Composite(expandItemComposite, SWT.NONE);
		plusLabel.addMouseListener(new MouseListener() {
			
			@Override
			public void mouseUp(MouseEvent e) {
				ElementListSelectionDialog listDialog = new ElementListSelectionDialog(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(), 
						new SelectorLabelProvider(klass));
				listDialog.setMultipleSelection(false);
				listDialog.setTitle("Select Object");
				listDialog.setMessage("Please select object you want to add to the session");
				listDialog.setElements(SystemObjectGUIProvidersHelper.getInstance().getAllIdsForClass(klass).toArray());
				
				int open = listDialog.open();
				if (open != Window.OK) return;
				String selectedId = (String) listDialog.getResult()[0];
				ISystemObjectGUIProvider provider = SystemObjectGUIProvidersHelper.getInstance().getProvider(klass, selectedId);
				int newIndex = provider.addObjectToSession(session, selectedId);
				if (newIndex < 0) return;
				
				createSystemObjectComponent(objectsComposite, provider, selectedId, newIndex);
				int y = expandItemComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT).y;
				expandItem.setHeight(y);
				parent.layout(true, true);
				validate();
				dirty = true;
				firePropertyChange(PROP_DIRTY);
			}
			
			@Override
			public void mouseDown(MouseEvent e) {
				// ignore
			}
			
			@Override
			public void mouseDoubleClick(MouseEvent e) {
				// ignore
			}
		});
		
		RowLayout dataLayout = new RowLayout();
		dataLayout.marginLeft = dataLayout.marginRight = dataLayout.marginBottom = 10;
		dataLayout.marginTop = 0;
		dataLayout.spacing = 15;
		dataLayout.wrap = true;
		objectsComposite.setLayout(dataLayout);
		
		int i = 0;
		for (String objectId : objectIds) {
			ISystemObjectGUIProvider guiProvider = SystemObjectGUIProvidersHelper.getInstance().getProvider(klass, objectId);
			if (guiProvider == null) {
				throw new IllegalArgumentException("There is no GUI provider registered for object '" + objectId + "' klass " + klass + ". Cannot display session!");
			}
			createSystemObjectComponent(objectsComposite, guiProvider, objectId, i);
			i++;
		}
		
		expandItem.setText(label);
		expandItem.setControl(expandItemComposite);
		expandItem.setHeight(expandItemComposite.computeSize(SWT.DEFAULT, SWT.DEFAULT).y);
		expandItem.setExpanded(expanded);
	}

	private void createSystemObjectComponent(final Composite parent, final ISystemObjectGUIProvider provider, String objectId, final int index) {
		GridData gd;
		
		final Composite composite = new Composite(parent, SWT.BORDER);
		GridLayout gridLayout = new GridLayout(4, false);
		composite.setLayout(gridLayout);
		
		Label errorLabel = new Label(composite, SWT.NONE);
		gd = new GridData(SWT.LEFT, SWT.CENTER, false, false);
		errorLabel.setLayoutData(gd);
		errorLabel.setImage(SessionGUIPlugin.getDefault().getImage(SessionGUIPlugin.NO_ERROR_IMG));
		validators.add(new SystemObjectValidator(index, errorLabel, provider));
		
		Label nameLabel = new Label(composite, SWT.LEFT);
		gd = new GridData(SWT.LEFT, SWT.CENTER, true, false);
		nameLabel.setLayoutData(gd);
		nameLabel.setText(provider.getDescription(session, index));
		
		Label settingsImg = new Label(composite, SWT.CENTER);
		gd = new GridData(SWT.RIGHT, SWT.CENTER, false, false);
		settingsImg.setLayoutData(gd);
		if (provider.isConfigurable(objectId)) {
			settingsImg.setImage(SessionGUIPlugin.getDefault().getImage(SessionGUIPlugin.SETTINGS_IMG));
			settingsImg.addMouseListener(new MouseListener() {
				
				@Override
				public void mouseUp(MouseEvent e) {
					boolean touched = provider.reconfigure(session, index);
					if (!touched) return;
					
					validate();
					dirty = true;
					firePropertyChange(PROP_DIRTY);
				}
				
				@Override
				public void mouseDown(MouseEvent e) {
					// ignore
				}
				
				@Override
				public void mouseDoubleClick(MouseEvent e) {
					// ignore
				}
				
			});
		} else {
			settingsImg.setImage(SessionGUIPlugin.getDefault().getImage(SessionGUIPlugin.SETTINGS_DISABLED_IMG));
		}
		
		Label removeImg = new Label(composite, SWT.CENTER);
		gd = new GridData(SWT.RIGHT, SWT.CENTER, false, false);
		removeImg.setLayoutData(gd);
		removeImg.setImage(SessionGUIPlugin.getDefault().getImage(SessionGUIPlugin.REMOVE_IMG));
		removeImg.addMouseListener(new MouseListener() {
			
			@Override
			public void mouseUp(MouseEvent e) {
				if (!MessageDialog.openConfirm(PlatformUI.getWorkbench().getActiveWorkbenchWindow().getShell(), 
						"Confirm", "Do you really want to remove this object?")) return;
				boolean touched = provider.remove(session, index);
				if (!touched) return;
				
				for (Iterator<SystemObjectValidator> it = validators.iterator(); it.hasNext();) {
					SystemObjectValidator validator = it.next();
					if (validator.getProvider().getProviderClass() == provider.getProviderClass() && validator.getIndex() == index) {
						it.remove();
					}
				}
				
				composite.dispose();
				parent.layout(true);
				validate();
				dirty = true;
				firePropertyChange(PROP_DIRTY);
			}
			
			@Override
			public void mouseDown(MouseEvent e) {
				// ignore
			}
			
			@Override
			public void mouseDoubleClick(MouseEvent e) {
				// ignore
			}
			
		});
		
		Label bigImage = new Label(composite, SWT.CENTER);
		gd = new GridData(SWT.CENTER, SWT.CENTER, true, true);
		gd.horizontalSpan = 4;
		bigImage.setLayoutData(gd);
		bigImage.setImage(provider.getImage(objectId));
		if (provider instanceof IOpenable) {
			bigImage.addMouseListener(new MouseListener() {
				
				@Override
				public void mouseUp(MouseEvent e) {
					// ignore
				}
				
				@Override
				public void mouseDown(MouseEvent e) {
					// ignore
				}
				
				@Override
				public void mouseDoubleClick(MouseEvent e) {
					((IOpenable) provider).openResource(session, index);
				}
				
			});
		}
		
		if (provider instanceof ISystemObjectChangeNotifier) {
			((ISystemObjectChangeNotifier) provider).registerListener(new ISystemObjectChangeListener() {
				
				@Override
				public void notifyChanged() {
					validate();
				}
				
			}, session, index);
		}
	}

	private void createDevicesPart(ExpandBar parent) {
		List<String> ids = new ArrayList<>();
		for (DeviceSessionContainer device : session.getDevices()) {
			ids.add(device.getDeviceId());
		}
		createExpandingPart(parent, SystemObjectGUIProviderClass.DEVICE, ids, "Devices", true);
	}

	private void createModulesPart(ExpandBar expandBar) {
		List<String> ids = new ArrayList<>();
		for (ModuleSessionContainer module : session.getModules()) {
			ids.add(module.getModuleId());
		}
		createExpandingPart(expandBar, SystemObjectGUIProviderClass.MODULE, ids, "Modules", true);
	}

	private void createMonitorsPart(ExpandBar expandBar) {
		createExpandingPart(expandBar, SystemObjectGUIProviderClass.MONITOR, session.getMonitors(), "Monitors", true);
	}

	private void createProgramsPart(ExpandBar expandBar) {
		List<String> ids = new ArrayList<>();
		for (ProgramSessionContainer program : session.getPrograms()) {
			ids.add(program.getProgramId());
		}
		createExpandingPart(expandBar, SystemObjectGUIProviderClass.PROGRAM, ids, "Programs", true);
	}

	@Override
	public void setFocus() {
		getSite().getPage().activate(this);
	}
	
	public boolean validate() {
		if (StringUtils.isNullOrEmpty(name.getText())) {
			errorMessage.setText("Name must be filled");
			return false;
		}
		
		boolean result = true;
		for (SystemObjectValidator validator : validators) {
			if (!validator.validate()) {
				result = false;
			}
		}
		
		if (result) {
			errorMessage.setText("");
		} else {
			errorMessage.setText("One or more objects below have an error. Please check for error markers");
		}
		return result;
	}
	
	private static class SelectorLabelProvider extends BaseLabelProvider implements ILabelProvider {

		private final SystemObjectGUIProviderClass objectKlass;

		public SelectorLabelProvider(SystemObjectGUIProviderClass objectKlass) {
			this.objectKlass = objectKlass;
		}
		
		@Override
		public Image getImage(Object element) {
			return null;
		}

		@Override
		public String getText(Object element) {
			Assert.isLegal(element instanceof String, "Expecting string id!");
			String id = (String) element;
			ISystemObjectGUIProvider provider = SystemObjectGUIProvidersHelper.getInstance().getProvider(objectKlass, id);
			if (provider == null) {
				throw new IllegalStateException("Cannot find provider for element " + element + " and klass " + objectKlass + ". This is strange!");
			}
			return provider.getDescription(id);
		}
		
	}
	
	private class SystemObjectValidator {
		
		private final int index;
		private final Label errorLabel;
		private final ISystemObjectGUIProvider provider;
		
		public SystemObjectValidator(int index, Label errorLabel, ISystemObjectGUIProvider provider) {
			this.index = index;
			this.errorLabel = errorLabel;
			this.provider = provider;
		}
		
		public boolean validate() {
			String message = provider.validate(session, index);
			if (message == null) {
				errorLabel.setImage(SessionGUIPlugin.getDefault().getImage(SessionGUIPlugin.NO_ERROR_IMG));
				errorLabel.setToolTipText("There are no errors");
				return true;
			}
			errorLabel.setImage(SessionGUIPlugin.getDefault().getImage(SessionGUIPlugin.ERROR_IMG));
			errorLabel.setToolTipText(message);
			return false;
		}

		public int getIndex() {
			return index;
		}

		public ISystemObjectGUIProvider getProvider() {
			return provider;
		}
		
	}
	
}

